文章目录
  1. 1. 背景
  2. 2. 问题描述
  3. 3. 总结下

背景

开发CloudClient的阿里云上传下载模块,本来以为可以愉快的使用阿里云的各种封装好的API,没想到刚刚开始开发下载文件就遇到了问题。阿里云并没有提供具体的根据用户名密码列举该用户名下所有Bucket的方法。

通过阅读其官网描述的API,发现可以基于RESTful的架构自己向阿里云服务器发送请求,从响应消息中解析所需要的信息。列举所以用户名下所有Bucket的方式是进行GetService操作需要构造一个格式如下的Http请求报文

1
2
3
4
GET / HTTP/1.1
Host: oss.aliyuncs.com
Date: GMT Date
Authorization: SignatureValue

其他的都不难理解,就唯独其中的签名Authorization: SignatureValue的生成让人费解,光是看阿里云提供的签名生成方法就花了很大精力。

1
2
3
4
5
6
7
8
9
"Authorization: OSS " + Access Key Id + ":" + Signature

Signature = base64(hmac-sha1(AccessKeySecret,
VERB + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedOSSHeaders
+ CanonicalizedResource))

花了很长时间研究CONTENT-MD5、CONTENT-TYPE到底是什么,还有给出的例子中明明是先用RFC 2104中定义的HMAC-SHA1方法以Access Key Secret为密钥加密然后再BASE64编码成字符串就可以了,但在下面的Python实现中还用到digest方法计算加密后数据的MD5,到底那个是正确的让人很迷惑。

1
2
3
4
5
6
import base64
import hmac
import sha
h = hmac.new("OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV",
"PUT\nODBGOERFMDMzQTczRUY3NUE3NzA5QzdFNUYzMDQxNEM=\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-oss-magic:abracadabra\nx-oss-meta-author:foo@bar.com\n/oss-example/nelson", sha)
base64.encodestring(h.digest()).strip()

问题描述

综上所述,在花了很长时间理解阿里云的文档之后,开始实现getService服务。第一步,需要通过HttpClient构建一个HttpGet消息。

1
2
3
4
5
6
7
8
9
10
HttpClient httpClient = new DefaultHttpClient();
String host = "oss.aliyuncs.com";

URI url = URIUtils.createURI("http", host, -1, "/", null, null);//ublic static URI createURI(String scheme,String host,int port,String path,String query,String fragment)
LogUtil.i("QueryAliyun:sendHttpGet", url+"");

HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Date", getSystemTime());
httpGet.addHeader("Host","oss.aliyuncs.com");
httpGet.addHeader("Authorization", getSignature());

第二步,需要按照文档中描述的方式对本次请求进行加密。问题就出现在这一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private String getSignature(){
String encryptText = "GET" + "\n"
+ "" + "\n"
+ "" + "\n"
+ getSystemTime() + "\n"
+ ""
+ "/";

String signature = "";
try {
String signatureTemp = ClientUtils.HmacSHA1Encrypt(InfoContainer.ALIYUN_SCRECT_ID, encryptText);

signature = Base64.encodeToString(signatureTemp.getByte(), Base64.DEFAULT).trim();

} catch (Exception e) {
LogUtil.e("QueryAliyun:getSignature", " catch exception in HmacSHA1Encrypt");
e.printStackTrace();
}
String authorization = "OSS " + InfoContainer.ALIYUN_ACCESS_ID + ":" + signature;
return authorization;
}

我将要加密的文本按照指定的算法进行加密,返回加密结果字符串,再使用BASE64方式编码,与服务器通信后始终是403错误。研究了很久,发现使用阿里云提供的方法对同样的内容进行签名操作就可以通信,于是我将该方法的结果进行BASE64解码,发现解码的结果和使用ClientUtils.HmacSHA1Encrypt方法放回的字符串一样。根据这个结论进一步研究猜测问题出现在signatureTemp.getByte()这个方法上,我将加密的结果作为byte[]返回,而不是先转换成字符串在装换成byte数组,再进行BASE64编码就可以正常通信了。

这让我怀疑通过new String()得到的字符串,再进行getByte[]操作无法得到原有的byte数组。因为new String和getByte方法在不指定编码方式的情况下都是使用当前环境的编码方式,这里是UTF-8,于是我使用了下面的方法进行测试

1
2
3
4
5
6
7
8
9
private void testGetbyte(byte[] signatureTemp) {
String testString;
try {
testString = new String(signatureTemp,"UTF-8");
byte[] testBytes = testString.getBytes("UTF-8");
LogUtil.i("QueryAliyun:testGetbyte", signatureTemp + " and " + testBytes);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

发现在utf-8的编码规则下,将byte数组装换成字符串在将字符串转换成byte数组,源数组不等于操作过后的数组。进一步研究,发现原因是因为UTF-8编码方式并不是采用单字节的编码方式进行转换,如果将这里的编码方式改成ISO-8859-1,源数组就和操作后的得到的数组一致了。这个坑也是导致我在这个地方花费了很长时间。

第三步,解析返回的数据

总结下

这里虽然ISO-8859-1来编码可以解码出原来的byte数组,但是使用不管什么内容,都用new String(…,”ISO-8859-1”)来建立字符串,然后使用的时候按默认的编码格式(通常在服务器上都是英文系统)输出字符串。这样其实你使用的String并不是按UNICODE来代表真正的字符,而是强行把BYTE数组复制到String的char[]里,一旦你的运行环境改变,你就被迫要修改一大堆的代码。而且也无法在同一个字符串里处理几种不同编码的文字。
另一个是把一种编码格式的字符串,比如是GB2312,转换成另一种格式的字符串,比如UTF-8,然后不指明是UTF-8编码,而直接用new String(…)来建立String,这样放在String里面的字符也是无法确定的,它在不同的系统上代表不同的字符。如果要求别人用“UTF-8格式”的String来交换信息的时候,其实已经破坏了JAVA为了兼容各种语言所做的规定。这种错误的本质思想是还按写C语言的方式,把字符串纯粹当作可以自己自由编码的存储器使用,而忽略了JAVA字符串只有一种编码格式。如果真的想自由编码,用byte[]或者char[]就完全了解决问题的了。

这里不得不吐槽下阿里云的Android API,对各项操作的封装不全也就算了,文档中几乎没有说明性的描述,绝大部分函数都没有任何的说明的文字,传入的形参和返回值也只告诉你类型,使用基本靠猜。即使对自己的命名再有信心,几句说明还是应该有的,具体可以看看AWS的API。
image